program MouseAndJoystick;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils, System.Math,
  SampleCommon,
  DIrr in '..\..\DSource\DIrr.pas',
  DIrr.GUI in '..\..\DSource\DIrr.GUI.pas',
  DIrr.Color in '..\..\DSource\DIrr.Color.pas',
  DIrr.Rect in '..\..\DSource\DIrr.Rect.pas',
  DIrr.Event in '..\..\DSource\DIrr.Event.pas',
  DIrr.Vector2D in '..\..\DSource\DIrr.Vector2D.pas',
  DIrr.Vector3D in '..\..\DSource\DIrr.Vector3D.pas',
  DIrr.Video in '..\..\DSource\DIrr.Video.pas',
  DIrr.Base in '..\..\DSource\DIrr.Base.pas',
  DIrr.Types in '..\..\DSource\DIrr.Types.pas',
  DIrr.Scene in '..\..\DSource\DIrr.Scene.pas',
  DIrr.Structs in '..\..\DSource\DIrr.Structs.pas',
  DIrr.Aabbox3D in '..\..\DSource\DIrr.Aabbox3D.pas',
  DIrr.Matrix4 in '..\..\DSource\DIrr.Matrix4.pas',
  DIrr.Texture in '..\..\DSource\DIrr.Texture.pas',
  DIrr.Dimension2d in '..\..\DSource\DIrr.Dimension2D.pas',
  DIrr.Collections in '..\..\DSource\DIrr.Collections.pas',
  DIrr.ConcreteMesh in '..\..\DSource\DIrr.ConcreteMesh.pas',
  DIrr.Quake3 in '..\..\DSource\DIrr.Quake3.pas',
  DIrr.Buffer in '..\..\DSource\DIrr.Buffer.pas',
  DIrr.IO in '..\..\DSource\DIrr.IO.pas',
  DIrr.Line3D in '..\..\DSource\DIrr.Line3D.pas',
  DIrr.Plane3D in '..\..\DSource\DIrr.Plane3D.pas',
  DIrr.Math in '..\..\DSource\DIrr.Math.pas',
  DIrr.Triangle3D in '..\..\DSource\DIrr.Triangle3D.pas',
  DIrr.Consts in '..\..\DSource\DIrr.Consts.pas';

type
  TMyEventReceiver = class(TEventReceiver)
  public type
    PMouseState = ^SMouseState;
    SMouseState = record
      Position: TPosition2D;
      LeftButtonDown: Boolean;
    end;
  private
    FMouseState: SMouseState;
    FJoystickState: SEvent.SJoystickEvent;
  public
    function OnEvent(const Event: Sevent): Boolean; override;
  public
    function GetJoystickState: SEvent.PJoystickEvent; inline;
    function GetMouseState: PMouseState; inline;
  end;

const
  MOVEMENT_SPEED: F32 = 5.0;
  DEAD_ZONE: F32 = 0.05;

var
  DriverType: E_DRIVER_TYPE;
  Receiver: TMyEventReceiver;
  Device: TIrrlichtDevice;
  Smgr: TSceneManager;
  Driver: TVideoDriver;
  Node: TSceneNode;
  Camera: TCameraSceneNode;
  theT, nowT: U32;
  FrameDeltaTime: F32;
  MovedWithJoystick: Boolean = False;
  NodePosition: TVector3DF;
  MoveHorizontal: F32 = 0;
  MoveVertical: F32 = 0;
  JoystickDataPtr: SEvent.PJoystickEvent;
  PovDegrees: U16;
  Ray: TLine3DF;
  Plane: TPlane3DF;
  MousePosition, ToMousePosition: TVector3DF;
  AvailableMovement: F32;
  MouseStatePtr: TMyEventReceiver.PMouseState;
  TmpStr: String;
  JoystickInfo: TJoystickInfoArray = nil;
  JoystickIdx: S32;
  Joystick: SJoystickInfo;
  SceneCollisionMgr: TSceneCollisionManager;

{ TMyEventReceiver }

function TMyEventReceiver.GetJoystickState: SEvent.PJoystickEvent;
begin
  Result := @FJoystickState;
end;

function TMyEventReceiver.GetMouseState: PMouseState;
begin
  Result := @FMouseState;
end;

function TMyEventReceiver.OnEvent(const Event: Sevent): Boolean;
begin
  // Remember the mouse state
  if Event.EventType = EET_MOUSE_INPUT_EVENT then
  begin
    case Event.MouseInput.Event of
      EMIE_LMOUSE_PRESSED_DOWN: FMouseState.LeftButtonDown := True;
      EMIE_LMOUSE_LEFT_UP: FMouseState.LeftButtonDown := False;
      EMIE_MOUSE_MOVED:
      begin
        FMouseState.Position.X := Event.MouseInput.X ;
        FMouseState.Position.Y := Event.MouseInput.Y ;
      end;
    end;
  end;

  // The state of each connected joystick is sent to us
  // once every run() of the Irrlicht device.  Store the
  // state of the first joystick, ignoring other joysticks.
  // This is currently only supported on Windows and Linux.
  if (Event.EventType = EET_JOYSTICK_INPUT_EVENT) and
    (Event.JoystickEvent.Joystick = 0) then
  begin
    FJoystickState := Event.JoystickEvent;
  end;

  Result := False;
end;

begin
  try
    ReportMemoryLeaksOnShutdown := True;

    // ask user for driver
    DriverType := DriverChoiceConsole;
    if DriverType = EDT_COUNT then
    begin
      ExitCode := 1;
      Exit;
    end;
    //Instance of the EventReceiver
    Receiver := TMyEventReceiver.Create;

    //Initialise the engine
    Device := CreateDevice(DriverType, Dimension2DU(640, 480), 16,
      False, False, False, Receiver);

    if not Assigned(Device) then
    begin
      ExitCode := 1;
      Exit;
    end;

    if Device.ActiveJoysticksDW(JoystickInfo) then
    begin
      TmpStr := 'Joystick support is enabled and ' + IntToStr(JoystickInfo.Count) +
        ' joystick(s) are present.';
      Writeln(TmpStr);

      for JoystickIdx := 0 to JoystickInfo.Count - 1 do
      begin
        Joystick := JoystickInfo.Item[JoystickIdx]^;

        TmpStr := 'Joystick ' + IntToStr(JoystickIdx) + ':';
        Writeln(TmpStr);
        TmpStr := #9'Name: ''' + Joystick.Name + '''';
        Writeln(TmpStr);
        TmpStr := #9'Axes: ' + IntToStr(Joystick.Axes);
        Writeln(TmpStr);
        TmpStr := #9'Buttons: ' + IntToStr(Joystick.Buttons);
        Writeln(TmpStr);

        TmpStr := #9'Hat is: ';
        case Joystick.PovHat of
          POV_HAT_PRESENT: TmpStr := TmpStr + 'present';
          POV_HAT_ABSENT:TmpStr := TmpStr + 'absent';
          else
            TmpStr := TmpStr + 'unknown';
        end;
        Writeln(TmpStr);
      end;
    end
    else
    begin
      Writeln('Joystick support is not enabled.');
    end;

    TmpStr := 'Irrlicht Joystick Example (';
    if Assigned(JoystickInfo) then
      TmpStr := TmpStr + IntToStr(JoystickInfo.Count)
    else
      TmpStr := TmpStr + '0';
    TmpStr := TmpStr + ' joysticks)';
    Device.SetWindowCaption(TmpStr);

    Driver := Device.GetVideoDriver;
    Smgr := Device.GetSceneManager;

    (*
    We'll create an arrow mesh and move it around either with the joystick axis/hat,
    or make it follow the mouse pointer. *)
    Node := Smgr.AddMeshSceneNode
            (
              Smgr.AddArrowMesh
              (
                'Arrow', IrrColor(255, 0, 0), IrrColor(0, 255, 0), 16, 16,  2, 1.3, 0.1, 0.6
              )
            );
    Node.SetMaterialFlag(EMF_LIGHTING, False);

    Camera := Smgr.AddCameraSceneNode();
    Camera.SetPosition(Vector3DF(0, 0, -10));

    // As in example 04, we'll use framerate independent movement.
    theT := Device.GetTimer.GetTime;


    while Device.Run do
    begin
      // Work out a frame delta time.
      nowT := Device.GetTimer.GetTime;
      FrameDeltaTime := (nowT - theT) / 1000; // Time in seconds
      theT := nowT;

      MovedWithJoystick := False;

      NodePosition := Node.GetPosition^;

      if JoystickInfo.Count > 0 then
      begin
        MoveHorizontal := 0;    // Range is -1.f for full left to +1.f for full right
        MoveVertical := 0;      // -1.f for full down to +1.f for full up.

        JoystickDataPtr := Receiver.GetJoystickState;

        // We receive the full analog range of the axes, and so have to implement our
        // own dead zone.  This is an empirical value, since some joysticks have more
        // jitter or creep around the center point than others.  We'll use 5% of the
        // range as the dead zone, but generally you would want to give the user the
        // option to change this.
        MoveHorizontal := JoystickDataPtr^.Axis[axisX] / 32767;
        if Abs(MoveHorizontal) < DEAD_ZONE then
          MoveHorizontal := 0;

        MoveVertical := JoystickDataPtr^.Axis[axisY] / -32767;
        if Abs(MoveVertical) < DEAD_ZONE then
          MoveVertical := 0;

        // POV hat info is only currently supported on Windows, but the value is
        // guaranteed to be 65535 if it's not supported, so we can check its range.
        PovDegrees := JoystickDataPtr^.POV div 100;
        if PovDegrees < 360 then
        begin
          if (PovDegrees > 0) and (PovDegrees < 180) then
            MoveHorizontal := 1
          else
            MoveHorizontal := -1;

          if (PovDegrees > 90) and (PovDegrees < 270) then
            MoveVertical := -1
          else
            MoveVertical := 1;
        end;

        if (not (FEquals(MoveHorizontal, 0))) or (not (FEquals(MoveVertical, 0))) then
        begin
          NodePosition.X := NodePosition.X + MOVEMENT_SPEED * FrameDeltaTime * MoveHorizontal;
          NodePosition.Y := NodePosition.Y + MOVEMENT_SPEED * FrameDeltaTime * MoveVertical;
          MovedWithJoystick := True;
        end;
      end;

      // If the arrow node isn't being moved with the joystick, then have it follow the mouse cursor.
      if not MovedWithJoystick then
      begin
        // Create a ray through the mouse cursor.
        SceneCollisionMgr := Smgr.GetSceneCollisionManager;
        MouseStatePtr := Receiver.GetMouseState;
        SceneCollisionMgr.GetRayFromScreenCoordinates(Ray, MouseStatePtr^.Position, Camera);

        // And intersect the ray with a plane around the node facing towards the camera.
        Plane.Create(NodePosition, Vector3DF(0, 0, -1));
        if Plane.GetIntersectionWithLine(Ray.StartPos, Ray.GetVector, MousePosition) then
        begin
          // We now have a mouse position in 3d space; move towards it.
          ToMousePosition := MousePosition - NodePosition;
          AvailableMovement := MOVEMENT_SPEED * FrameDeltaTime;

          if ToMousePosition.GetLength <= AvailableMovement then
          begin
            NodePosition := MousePosition;   // Jump to the final position
          end
          else
          begin
            ToMousePosition.Normalize;
            NodePosition := NodePosition + ToMousePosition * AvailableMovement; // Move towards it
          end;
        end;
      end;

      Node.SetPosition(NodePosition);

      // Turn lighting on and off depending on whether the left mouse button is down.
      Node.SetMaterialFlag(EMF_LIGHTING, Receiver.GetMouseState.LeftButtonDown);

      Driver.BeginScene(True, True, IrrColor(113, 113, 113));
      Smgr.DrawAll;   // draw the 3d scene
      Driver.EndScene;
    end;

    Device.Drop;
    Receiver.Free;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.
